package api.betfairAPI;

import com.google.common.base.Optional;
import exceptions.ParseMarketPricesException;
import org.apache.log4j.Logger;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import static java.lang.Integer.parseInt;
import static java.lang.Long.parseLong;
import static java.lang.Double.parseDouble;

import com.betfair.www.publicapi.types.exchange.v5.Runner;

/**
 * User: Darren
 * Date: 28/01/13
 * Time: 12:46 PM
 * Description: this class contain the logic for decompressing the string returned by
 * com.betfair.www.publicapi.types.exchange.v5.GetMarketPricesCompressedResp#getMarketPrices()
 */
public class BetfairParseMarket {
    private static Logger logger = Logger.getLogger(BetfairParseMarket.class);
    private StringBuilder sb = new StringBuilder();
    private static final String dateFormat = "HH.mm";
    private SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);

    private static final int COMPRESSED_MARKET_ATTRIBUTES = 11;
    private static final int RUNNERS_ATTRIBUTES = 11;
    private static final int COMPRESSED_PRICE_ATTRIBUTES = 4;
    private static enum MarketAttributes {
        MARKET_NAME,
        MARKET_ID,
        EXCHANGE_NAME,
        EXCHANGE_ID,
        START_TIME,
        MARKET_STATUS,
        COUNTRY_CODE,
        RUNNER_SELECTION_ID,
        RUNNER_NAME
    }
    private static enum CompressedBetfairMarketPricesAttributes {
        MARKET_ID(0),
        CURRENCY(1),
        MARKET_STATUS(2),
        IN_PLAY_DELAY(3),
        NUMBER_OF_WINNERS(4),
        MARKET_INFORMATION(5),
        DISCOUNT_ALLOWED(6),
        MARKET_BASE_RATE(7),
        REFRESH_TIME(8),
        REMOVED_RUNNERS(9),
        SP_MARKET(0),
        RUNNERS(1),
        REMOVED_RUNNERS_NAME(0),
        REMOVED_RUNNERS_REMOVED_TIME(1),
        REMOVED_RUNNERS_REDUCTION_FACTOR(2),
        RUNNERS_SELECTION_ID(0),
        RUNNERS_ORDER_INDEX(1),
        RUNNERS_TOTAL_AMOUNT_MATCHED(2),
        RUNNERS_LAST_PRICE_MATCHED(3),
        RUNNERS_HANDICAP(4),
        RUNNERS_REDUCTION_FACTOR(5),
        RUNNERS_VACANT(6),
        RUNNERS_FAR_SP_PRICE(7),
        RUNNERS_NEAR_SP_PRICE(8),
        RUNNERS_ACTUAL_SP_PRICE(9),
        RUNNERS_BACK_PRICE(1),
        RUNNERS_LAY_PRICE(2),
        PRICE(0),
        PRICE_AMOUNT_AVAILABLE(1),
        PRICE_TYPE(2),
        PRICE_DEPTH(3);

        private int index;
        CompressedBetfairMarketPricesAttributes(int index) {
            this.index = index;
        }

        public int getIndex() {
            return index;
        }
    }

    private StringBuilder getStringBuilderInstance() {
        return sb.delete(0, sb.length());
    }

    private String logDebug(String str, CompressedBetfairMarketPricesAttributes attribute) {
        StringBuilder sb = getStringBuilderInstance();
        sb.append(attribute.name()).append(": ").append(str);
        logger.debug(sb.toString());
        return str;
    }
    private String logDebug(String str, MarketAttributes attribute) {
        StringBuilder sb = getStringBuilderInstance();
        sb.append(attribute.name()).append(": ").append(str);
        logger.debug(sb.toString());
        return str;
    }
    private int logDebug(int i, MarketAttributes attribute) {
        StringBuilder sb = getStringBuilderInstance();
        sb.append(attribute.name()).append(": ").append(Integer.toString(i));
        logger.debug(sb.toString());
        return i;
    }
    private Calendar logDebug(Calendar cal, MarketAttributes attribute) {
        StringBuilder sb = getStringBuilderInstance();
        sb.append(attribute.name()).append(": ").append(sdf.format(cal.getTime()));
        logger.debug(sb.toString());
        return cal;
    }

    private void marketInstantiated(BetfairMarket market) {
        if (market == null) {
            logger.error("The BetfairMarket object has not been instantiated");
            throw new ParseMarketPricesException();
        }
    }

    public void parseBetfairExchangeMarket(String compressedMarketPrices, BetfairExchangeMarket betfairExchangeMarket,
                                           BetfairMarket market) throws ParseException {
        marketInstantiated(market);
        setMarketName(betfairExchangeMarket.getMarket().getName(), market);
        setMarketId(betfairExchangeMarket.getMarket().getMarketId(), market);
        setExchangeName(betfairExchangeMarket.getExchangeName(), market);
        setExchangeId(betfairExchangeMarket.getExchangeId(), market);
        setStartTime(betfairExchangeMarket.getMarket().getMarketTime(), market);
        setMarketStatus(betfairExchangeMarket.getMarket().getMarketStatus().toString(), market);
        setCountryCode(betfairExchangeMarket.getMarket().getCountryISO3(), market);
        // Initiate the map of runners with their name
        Optional<Runner[]> runners = Optional.of(betfairExchangeMarket.getMarket().getRunners().getRunner());
        if (runners.isPresent()) {
            for (Runner mr: runners.get()) {
                setRunner(mr, market);
            }
        }
        parseCompressedMarketPrices(compressedMarketPrices, market);
    }

    private void setMarketName(String marketName, BetfairMarket market) {
        market.setMarketName(logDebug(marketName, MarketAttributes.MARKET_NAME));
    }
    private void setMarketId(int marketId, BetfairMarket market) {
        market.setMarketId(logDebug(marketId, MarketAttributes.MARKET_ID));
    }
    private void setExchangeName(String exchangeName, BetfairMarket market) {
        market.setExchangeName(logDebug(exchangeName, MarketAttributes.EXCHANGE_NAME));
    }
    private void setExchangeId(int exchangeId, BetfairMarket market) {
        market.setExchangeId(logDebug(exchangeId, MarketAttributes.EXCHANGE_ID));
    }
    private void setStartTime(Calendar startTime, BetfairMarket market) {
        market.setStartTime(logDebug(startTime, MarketAttributes.START_TIME));
    }
    private void setMarketStatus(String marketStatus, BetfairMarket market) {
        market.setStatus(logDebug(marketStatus, MarketAttributes.MARKET_STATUS));
    }
    private void setCountryCode(String countryCode, BetfairMarket market) {
        market.setCountry(logDebug(countryCode,  MarketAttributes.COUNTRY_CODE));
    }

    private void setRunner(Runner runner, BetfairMarket market) {
        market.getNewRunner(logDebug(runner.getSelectionId(), MarketAttributes.RUNNER_SELECTION_ID))
                .setName(logDebug(runner.getName(), MarketAttributes.RUNNER_NAME));
    }

    private void parseCompressedMarketPrices(String compressedMarketPrices, BetfairMarket market)
            throws ParseException {
        String[] tildaTokens = compressedMarketPrices.split("[~]", COMPRESSED_MARKET_ATTRIBUTES);
        market.setMarketId(readInt(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.MARKET_ID.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.MARKET_ID)));
        market.setCurrency(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.CURRENCY.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.CURRENCY));
        market.setMarketStatus(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.MARKET_STATUS.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.MARKET_STATUS));
        market.setInPlayDelay(readInt(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.IN_PLAY_DELAY.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.IN_PLAY_DELAY)));
        market.setNumberOrWinners(readInt(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.NUMBER_OF_WINNERS.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.NUMBER_OF_WINNERS)));
        market.setMarketInformation(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.MARKET_INFORMATION.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.MARKET_INFORMATION));
        market.setDiscountAllowed(readBoolean(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.DISCOUNT_ALLOWED.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.DISCOUNT_ALLOWED)));
        market.setMarketBaseRate(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.MARKET_BASE_RATE.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.MARKET_BASE_RATE));
        market.setRefreshTime(readLong(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.REFRESH_TIME.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.REFRESH_TIME)));
        parseRemovedRunners(
                logDebug(readString(tildaTokens[CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS), market);
        String[] colonTokens = tildaTokens[tildaTokens.length -1].split("[:]", 2);
        market.setbSPMarket(readBoolean(
                logDebug(readString(colonTokens[CompressedBetfairMarketPricesAttributes.SP_MARKET.getIndex()]),
                        CompressedBetfairMarketPricesAttributes.SP_MARKET)));
        if (colonTokens.length > CompressedBetfairMarketPricesAttributes.RUNNERS.getIndex()) {
            parseRunners(
                    logDebug(readString(colonTokens[CompressedBetfairMarketPricesAttributes.RUNNERS.getIndex()]),
                            CompressedBetfairMarketPricesAttributes.RUNNERS), market);
        }
    }

    private void parseRemovedRunners(String data, BetfairMarket market) throws ParseException {
        if (data.length() > 0) {
            String[] tokens = data.split("[;]");
            for (int i = 0; i < tokens.length; ++i) {
                Optional<BetfairMarket.RemovedRunner> removedRunnerOptional = Optional.of(market.getNewRemovedRunner());
                if (!removedRunnerOptional.isPresent()) {
                    logger.error("The RemovedRunner at the following index has not been initialised: " + i);
                    throw new ParseMarketPricesException();
                }
                parseRemovedRunner(tokens[i], removedRunnerOptional.get());
            }
        }
    }

    private void parseRemovedRunner(String data, BetfairMarket.RemovedRunner removedRunner) throws ParseException {
        if (data.length() > 0) {
            String[] tokens = data.split("[,]");
            removedRunner.setName(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS_NAME.getIndex()],
                            CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS_NAME));
            removedRunner.setRemovedDate(readDate(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS_REMOVED_TIME.getIndex()],
                            CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS_REMOVED_TIME)));
            removedRunner.setReductionFactor(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS_REDUCTION_FACTOR.getIndex()],
                            CompressedBetfairMarketPricesAttributes.REMOVED_RUNNERS_REDUCTION_FACTOR));
        }
    }

    private void parseRunners(String data, BetfairMarket market) {
        if (data.length() > 0) {
            String[] tokens = data.split("[:]");
            for (int i = 0; i < tokens.length; ++i) {
                String[] runnersData = tokens[i].split("[~]", 2);
                Optional<BetfairMarket.Runner> runnerOptional = market.getExistingRunner(readInt(
                        runnersData[CompressedBetfairMarketPricesAttributes.RUNNERS_SELECTION_ID.getIndex()]));
                if (!runnerOptional.isPresent()) {
                    logger.error("The Runner at the following index has not been initialised: " + i);
                    throw new ParseMarketPricesException();
                }
                parseRunner(tokens[i], runnerOptional.get());
            }
        }
    }

    private void parseRunner(String data, BetfairMarket.Runner runner) {
        if (data.length() > 0) {
            String[] tokens = data.split("[~]", RUNNERS_ATTRIBUTES);
            runner.setSelectionId(readInt(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_SELECTION_ID.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_SELECTION_ID)));
            runner.setOrderIndex(readInt(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_ORDER_INDEX.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_ORDER_INDEX)));
            runner.setTotalAmountMatched(readDouble(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_TOTAL_AMOUNT_MATCHED.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_TOTAL_AMOUNT_MATCHED)));
            runner.setLastPriceMatched(readDouble(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_LAST_PRICE_MATCHED.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_LAST_PRICE_MATCHED)));
            runner.setHandicap(readDouble(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_HANDICAP.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_HANDICAP)));
            runner.setReductionFactor(readDouble(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_REDUCTION_FACTOR.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_REDUCTION_FACTOR)));
            runner.setVacant(readBoolean(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_VACANT.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_VACANT)));
            runner.setFarSPPrice(readDouble(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_FAR_SP_PRICE.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_FAR_SP_PRICE)));
            runner.setNearSPPrice(readDouble(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_NEAR_SP_PRICE.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_NEAR_SP_PRICE)));
            runner.setActualSPPrice(readDouble(
                    logDebug(tokens[CompressedBetfairMarketPricesAttributes.RUNNERS_ACTUAL_SP_PRICE.getIndex()],
                            CompressedBetfairMarketPricesAttributes.RUNNERS_ACTUAL_SP_PRICE)));
            String[] prices = tokens[tokens.length -1].split("[|]");
            if (prices.length > CompressedBetfairMarketPricesAttributes.RUNNERS_BACK_PRICE.getIndex()) {
                String[] backPrices =
                        prices[CompressedBetfairMarketPricesAttributes.RUNNERS_BACK_PRICE.getIndex()].split("[~]");
                parsePrice(backPrices, runner, BetfairMarket.Runner.PriceType.BACK);
            }
            if (prices.length > CompressedBetfairMarketPricesAttributes.RUNNERS_LAY_PRICE.getIndex()) {
                String[] layPrices =
                        prices[CompressedBetfairMarketPricesAttributes.RUNNERS_LAY_PRICE.getIndex()].split("[~]");
                parsePrice(layPrices, runner, BetfairMarket.Runner.PriceType.LAY);
            }
        }
    }

    private void parsePrice(String[] backLayPrices, BetfairMarket.Runner runner,
                            BetfairMarket.Runner.PriceType backOrLayPrice) {
        if (backLayPrices.length < COMPRESSED_PRICE_ATTRIBUTES ||
                backLayPrices.length % COMPRESSED_PRICE_ATTRIBUTES != 0) {
            return;
        }
        BetfairMarket.Price backLayPrice;
        int priceCount = 0;
        for (int i = 0; i < backLayPrices.length; i += COMPRESSED_PRICE_ATTRIBUTES) {
            backLayPrice = runner.getPrice(priceCount, backOrLayPrice);
            priceCount++;
            backLayPrice.setPrice(readDouble(
                    logDebug(backLayPrices[CompressedBetfairMarketPricesAttributes.PRICE.getIndex() +i],
                            CompressedBetfairMarketPricesAttributes.PRICE)));
            backLayPrice.setAmountAvailable(readDouble(
                    logDebug(backLayPrices[CompressedBetfairMarketPricesAttributes.PRICE_AMOUNT_AVAILABLE.getIndex()+i],
                            CompressedBetfairMarketPricesAttributes.PRICE_AMOUNT_AVAILABLE)));
            backLayPrice.setType(
                    logDebug(backLayPrices[CompressedBetfairMarketPricesAttributes.PRICE_TYPE.getIndex() +i],
                            CompressedBetfairMarketPricesAttributes.PRICE_TYPE));
            backLayPrice.setDepth(readInt(
                    logDebug(backLayPrices[CompressedBetfairMarketPricesAttributes.PRICE_DEPTH.getIndex() +i],
                            CompressedBetfairMarketPricesAttributes.PRICE_DEPTH)));
        }
    }

    private boolean readBoolean(String str) {
        return str.equalsIgnoreCase("Y") || str.equalsIgnoreCase("true");
    }

    private String readString(String str) {
        StringBuilder sb = getStringBuilderInstance();
        for (int i = 0; i < str.length(); ++i) {
            char c = str.charAt(i);
            if (c == '\\') {
                // This is an escape character, read the next char
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    private Date readDate(String str) throws ParseException {
        try {
            return sdf.parse(str);
        } catch (ParseException e) {
            logger.error("Malformed compressed date: " + str + ", expected format: " + dateFormat);
            e.printStackTrace();
            throw e;
        }
    }

    private int readInt(String str) {
        try {
            return !str.equals("") ? parseInt(str) : 0;
        } catch (NumberFormatException e) {
            logger.error(e.toString());
            e.printStackTrace();
            return 0;
        }
    }

    private double readDouble(String str) {
        try {
            return !str.equals("") ? parseDouble(str) : 0;
        } catch (NumberFormatException e) {
            logger.error(e.toString());
            e.printStackTrace();
            return 0.0d;
        }
    }

    private long readLong(String str) {
        try {
            return !str.equals("") ? parseLong(str) : 0;
        } catch (NumberFormatException e) {
            logger.error(e.toString());
            e.printStackTrace();
            return 0L;
        }
    }
}
